Elixir1.5とPhoenix1.3の変更内容
Elixirのv1.5が7/25に、Phoenixのv1.3が7/29にそれぞれリリースされました。
遅くなりましたが、それぞれの変更点を公式ページを参考にしながら試してみます。
Elixir v1.5 released
Phoenix v1.3 released
Elixir 1.5
UTF-8 Atom function names and variables
アトムと変数にUTF-8を使用できるようになりました
test "ステータスコード200を受け取る" do assert handle_response(%HTTPoison.Response{status_code: 200, headers: %{}, body: "[\"body\"]"}) == ["body"] end
このテストケース名だと1.4まではエラーになります。
** (ArgumentError) argument error :erlang.binary_to_atom("test ステータスコード200を受け取る", :utf8) (ex_unit) lib/ex_unit/case.ex:411: ExUnit.Case.register_test/4
1.5では成功します。
Finished in 0.1 seconds 3 tests, 0 failures
IEx helpers and breakpoints
ElixirのREPLであるIExに様々な機能が追加されました
Autocompletion
変数やimportしたモジュールの関数が補完されるようになります
exports
モジュールの関数名を出力します
iex(1)> exports Enum all?/1 all?/2 any?/1 any?/2 at/2 at/3 chunk/2 chunk/3 chunk/4 chunk_by/2 chunk_every/2 chunk_every/3 chunk_every/4 chunk_while/4 concat/1 concat/2 count/1 count/2 dedup/1 dedup_by/2 drop/2 drop_every/2 drop_while/2 each/2 empty?/1 fetch/2 fetch!/2 filter/2 filter_map/3 find/2 find/3 find_index/2 find_value/2 find_value/3 flat_map/2 flat_map_reduce/3 group_by/2 group_by/3 intersperse/2 into/2 into/3 join/1 join/2 map/2 map_every/3 map_join/2 map_join/3 map_reduce/3 max/1 max/2 max_by/2 max_by/3 member?/2 min/1 min/2 min_by/2 min_by/3 min_max/1 min_max/2 min_max_by/2 min_max_by/3 partition/2 random/1 reduce/2 reduce/3 reduce_while/3 reject/2 reverse/1 reverse/2 reverse_slice/3 scan/2 scan/3 shuffle/1 slice/2 slice/3 sort/1 sort/2 sort_by/2 sort_by/3 split/2 split_while/2 split_with/2 sum/1 take/2 take_every/2 take_random/2 take_while/2 to_list/1 uniq/1 uniq/2 uniq_by/2 unzip/1 with_index/1 with_index/2 zip/1 zip/2
runtime_info
IEx実行時の情報を出力できる
iex(1) runtime_info ## System and architecture Elixir version: 1.5.0 OTP version: 20 ERTS version: 9.0 Compiled for: x86_64-apple-darwin15.6.0 Schedulers: 4 Schedulers online: 4 ## Memory Total: 19 MB Atoms: 258 kB Binaries: 88 kB Code: 6927 kB ETS: 382 kB Processes: 4855 kB ## Statistics / limits Uptime: 3 minutes and 4 seconds Run queue: 0 Atoms: 10234 / 1048576 (0% used) ETS: 20 / 2053 (0% used) Ports: 5 / 65536 (0% used) Processes: 47 / 262144 (0% used)
IEx brakpoints
IExでbrakpointが貼れるようになりました。
簡単なデバッグであれば require IEx; IEx.pry
が不要になりそうです。
iex(1)> break! Ehee.Gists.breakpoint_test/1 1 iex(5)> Ehee.Gists.brakpoint_test("世界") Break reached: Ehee.Gists.brakpoint_test/1 (lib/ehee/gists.ex:8) 6: """ 7: 8: def brakpoint_test(message) do 9: IO.puts("こんにちは #{message}") 10: end pry(1)> message "世界" pry(2)> whereami Location: lib/ehee/gists.ex:8 6: """ 7: 8: def brakpoint_test(message) do 9: IO.puts("こんにちは #{message}") 10: end
Exception.blame
Debug情報を特定の例外に付けることができる機能です。
現在はFunctionClauseErrorのどの部分が一致しどの部分が一致しなかったかを説明するために使われています。
(赤字がマッチしなかった部分を表しています)
Streamlined child specs
Supervisorの定義をModule指定でできるようになりました。以前より簡潔に記述することができます。
children = [ MyApp.Repo, MyApp.Endpoint ] Supervisor.start_link(children, strategy: :one_for_one)
引数が必要な場合はtupleで指定します。
children = [ {MyApp.Repo, url: "ecto://localhost:4567/my_dev"}, MyApp.Endpoint ]
@impl
どの関数がコールバックの実装かを@impl
でマークできるようになりました。
@implでコールバック関数をマークすることで他の関数と区別しやすくできます。
以下はPlugプロジェクトの例です。
defmodule MyApp do @behaviour Plug @impl true def init(_opts) do opts end @impl true def call(conn, _opts) do Plug.Conn.send_resp(conn, 200, "hello world") end
@impl属性を付けると以下の利点があります。
@impl true
属性を付けると自動的に@doc false
とマークし、@doc
属性を明示的につけない限りドキュメントを無効にする@impl
をコールバックではない関数にマークするとエラーになる(タイプミス、モジュールの動作定義が変更になった場合に気づきやすくなる)- ある実装で
@impl
属性を付けると、同じモジュールの他のすべての実装にも@impl
属性を付ける
Phoenix 1.3
続けて、Phoenixの変更点です。
Phx.new
- 1.3からジェネレーターのコマンドが
phx
になりました。以前のphoenix
コマンドは1.4で削除されます - 1.2までは
lib
と同階層にweb
ディレクトリがありましたが、1.3からはlibの下にmy_app_web
ディレクトリができその中にcontrollers
,views
,templates
,channels
が置かれるようになりました。
Contexts
1.3の大きな変更点です。ディレクトリ構成が従来のMVCではなくなりました。
ビジネスロジックを記述するlib/my_app
ディレクトリとwebに関するモジュール(controller, view, template, channel等)を置くlib/my_app_web
ディレクトリに別れました。
userリソースを作成してみます。コンテキスト名も指定します。
$ mix phx.gen.json Accounts User users email:string:unique
lib/my_app/
の下にAccounts
ディレクトリとその中にAccounts
モジュールが作成されました。
このモジュールに認証やユーザー登録のようなビジネスロジックを記述します。
初期生成時のコードは以下です
defmodule MyApp.Accounts do @moduledoc """ The Accounts context. """ import Ecto.Query, warn: false alias MyApp.Repo alias MyApp.Accounts.User def list_users do Repo.all(User) end def get_user!(id), do: Repo.get!(User, id) def create_user(attrs \\ %{}) do %User{} |> User.changeset(attrs) |> Repo.insert() end def update_user(%User{} = user, attrs) do user |> User.changeset(attrs) |> Repo.update() end def delete_user(%User{} = user) do Repo.delete(user) end def change_user(%User{} = user) do User.changeset(user, %{}) end end
Schemaはaccounts/user.ex
に定義されています
defmodule MyApp.Accounts.User do use Ecto.Schema import Ecto.Changeset alias MyApp.Accounts.User schema "users" do field :email, :string timestamps() end @doc false def changeset(%User{} = user, attrs) do user |> cast(attrs, [:email]) |> validate_required([:email]) |> unique_constraint(:email) end end
ControllerからはAccountsモジュールを経由してユーザー作成、ユーザー取得ロジックを呼び出しています。
他のコントローラからアカウント操作に関するビジネスロジックの再利用がやりやすくなることが目的です。
def index(conn, _params) do users = Accounts.list_users() render(conn, "index.html", users: users) end def new(conn, _params) do changeset = Accounts.change_user(%User{}) render(conn, "new.html", changeset: changeset) end def create(conn, %{"user" => user_params}) do case Accounts.create_user(user_params) do {:ok, user} -> conn |> put_flash(:info, "User created successfully.") |> redirect(to: user_path(conn, :show, user)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end
Contextを導入することの利点ですが、公式サイトでは以下のように説明しています。
- アプリケーション(のロジック)が適切に分離され、メンテナンス、再利用しやすくなる
- 以前の
web/models
ディレクトリの下にビジネスロジックが置かれている場合はファイルの関係性が見ただけではわからなかったが、Contextを導入することにより関係性が分かりやすくなりアプリケーションの見通しが良くなる- 例えばcontextsディレクトリに
accounts
,sales
ディレクトリがあればこのアプリケーションにはアカウントシステムとセールスシステムがあることがコードを見なくてもすぐに分かる。
- 例えばcontextsディレクトリに
action_fallback
controllerのactionが失敗した場合の処理をまとめて一つの場所に記述できるようになりました。
以前までは複数のコントローラーで同じようなエラー処理を書いていました。
def MyAppWeb.PageController do alias MyApp.CMS def show(conn, %{"id" => id}) do case CMS.get_page(id, conn.assigns.current_user) do {:ok, page} -> render(conn, "show.html", page: page) {:error, :not_found} -> conn |> put_status(404) |> render(MyAppWeb.ErrorView, :"404") {:error, :unauthorized} -> conn |> put_status(401) |> render(MyAppWeb.ErrorView, :"401") end end end
1.3ではこう書くことができるようになりました。
def MyAppWeb.PageController do alias MyApp.CMS action_fallback MyAppWeb.FallbackController def show(conn, %{"id" => id}) do with {:ok, page} <- CMS.get_page(id, conn.assigns.current_user) do render(conn, "show.html", page: page) end end end defmodule MyAppWeb.FallbackController do def call(conn, {:error, :not_found}) do conn |> put_status(:not_found) |> render(MyAppWeb.ErrorView, :"404") end def call(conn, {:error, :unauthorized}) do conn |> put_status(:unauthorized) |> render(MyAppWeb.ErrorView, :"401") end end
- action_falbackでエラー処理を行うモジュールを指定する
- controllerに
actin_fallback
を指定し、with式でアクションの処理を記述する - エラー処理を一箇所にまとめることができる
Elixir1.2で入ったwith記法がうまく使われています。
まとめ
Elixirはv1.2で大きな変更がありましたが、その後は機能をより良くするようなアップデートが続いています。
v1.5ではユニコード対応やREPLのアップデートなど、プログラマにとってありがたい進化が多い印象です。
一方、Phoenixのv1.3はディレクトリ構造の変更など大きな変更が入り、今までのRailsっぽいフレームワークという印象から大きく変わります。
ファイルの置き場所に最初は迷いも出てくるかもしれませんが、再利用性、見通しの良さといった点が規模の大きなアプリケーションには大きな利点になりそうかなと思いました。